{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<font color=\"red\">注</font>: 使用 tensorboard 可视化需要安装 tensorflow (TensorBoard依赖于tensorflow库，可以任意安装tensorflow的gpu/cpu版本)\n",
    "\n",
    "```shell\n",
    "pip install tensorflow-cpu\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:37.505092Z",
     "start_time": "2025-01-20T06:39:28.540647Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:30:07.172756Z",
     "iopub.status.busy": "2025-01-21T02:30:07.172603Z",
     "iopub.status.idle": "2025-01-21T02:30:09.114210Z",
     "shell.execute_reply": "2025-01-21T02:30:09.113720Z",
     "shell.execute_reply.started": "2025-01-21T02:30:07.172739Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=10, micro=14, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.5.1+cu124\n",
      "cuda:0\n"
     ]
    }
   ],
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据准备\n",
    "\n",
    "```shell\n",
    "$ tree -L 2 archive \n",
    "archive\n",
    "├── monkey_labels.txt\n",
    "├── training\n",
    "│   ├── n0\n",
    "│   ├── n1\n",
    "│   ├── n2\n",
    "│   ├── n3\n",
    "│   ├── n4\n",
    "│   ├── n5\n",
    "│   ├── n6\n",
    "│   ├── n7\n",
    "│   ├── n8\n",
    "│   └── n9\n",
    "└── validation\n",
    "    ├── n0\n",
    "    ├── n1\n",
    "    ├── n2\n",
    "    ├── n3\n",
    "    ├── n4\n",
    "    ├── n5\n",
    "    ├── n6\n",
    "    ├── n7\n",
    "    ├── n8\n",
    "    └── n9\n",
    "\n",
    "22 directories, 1 file\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.752355Z",
     "start_time": "2025-01-20T06:39:37.505092Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:30:53.761262Z",
     "iopub.status.busy": "2025-01-21T02:30:53.760939Z",
     "iopub.status.idle": "2025-01-21T02:30:53.772821Z",
     "shell.execute_reply": "2025-01-21T02:30:53.772330Z",
     "shell.execute_reply.started": "2025-01-21T02:30:53.761240Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "load 1097 images from training dataset\n",
      "load 272 images from validation dataset\n"
     ]
    }
   ],
   "source": [
    "from torchvision import datasets\n",
    "from torchvision.transforms import ToTensor, Resize, Compose, ConvertImageDtype, Normalize\n",
    "\n",
    "\n",
    "from pathlib import Path\n",
    "\n",
    "DATA_DIR1 = Path(\"/content/training/\")\n",
    "DATA_DIR2 = Path(\"/content/validation/\")\n",
    "\n",
    "class MonkeyDataset(datasets.ImageFolder):\n",
    "    def __init__(self, mode, transform=None):\n",
    "        if mode == \"train\":\n",
    "            root = DATA_DIR1 / \"training\"\n",
    "        elif mode == \"val\":\n",
    "            root = DATA_DIR2 / \"validation\"\n",
    "        else:\n",
    "            raise ValueError(\"mode should be one of the following: train, val, but got {}\".format(mode))\n",
    "        super().__init__(root, transform)\n",
    "        self.imgs = self.samples\n",
    "        self.targets = [s[1] for s in self.samples]\n",
    "\n",
    "# resnet 要求的，见 https://pytorch.org/vision/stable/models/generated/torchvision.models.resnet50.html\n",
    "img_h, img_w = 224, 224\n",
    "transform = Compose([\n",
    "     Resize((img_h, img_w)),\n",
    "     ToTensor(),\n",
    "     Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),\n",
    "     ConvertImageDtype(torch.float),\n",
    "])\n",
    "\n",
    "\n",
    "train_ds = MonkeyDataset(\"train\", transform=transform)\n",
    "val_ds = MonkeyDataset(\"val\", transform=transform)\n",
    "\n",
    "print(\"load {} images from training dataset\".format(len(train_ds)))\n",
    "print(\"load {} images from validation dataset\".format(len(val_ds)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.756387Z",
     "start_time": "2025-01-20T06:39:38.752355Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:30:53.774343Z",
     "iopub.status.busy": "2025-01-21T02:30:53.773924Z",
     "iopub.status.idle": "2025-01-21T02:30:53.779007Z",
     "shell.execute_reply": "2025-01-21T02:30:53.778272Z",
     "shell.execute_reply.started": "2025-01-21T02:30:53.774320Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9'],\n",
       " {'n0': 0,\n",
       "  'n1': 1,\n",
       "  'n2': 2,\n",
       "  'n3': 3,\n",
       "  'n4': 4,\n",
       "  'n5': 5,\n",
       "  'n6': 6,\n",
       "  'n7': 7,\n",
       "  'n8': 8,\n",
       "  'n9': 9})"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 数据类别\n",
    "train_ds.classes, train_ds.class_to_idx"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.775815Z",
     "start_time": "2025-01-20T06:39:38.756387Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:30:53.780024Z",
     "iopub.status.busy": "2025-01-21T02:30:53.779675Z",
     "iopub.status.idle": "2025-01-21T02:30:53.840743Z",
     "shell.execute_reply": "2025-01-21T02:30:53.840128Z",
     "shell.execute_reply.started": "2025-01-21T02:30:53.779996Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/content/training/training/n0/n0018.jpg 0\n",
      "torch.Size([3, 224, 224]) 0\n"
     ]
    }
   ],
   "source": [
    "# 图片路径 及 标签\n",
    "for fpath, label in train_ds.imgs:\n",
    "    print(fpath, label)\n",
    "    break\n",
    "\n",
    "for img, label in train_ds:\n",
    "    # c, h, w  label\n",
    "    print(img.shape, label)\n",
    "    break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.778866Z",
     "start_time": "2025-01-20T06:39:38.775815Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:30:53.841769Z",
     "iopub.status.busy": "2025-01-21T02:30:53.841408Z",
     "iopub.status.idle": "2025-01-21T02:30:53.845432Z",
     "shell.execute_reply": "2025-01-21T02:30:53.844886Z",
     "shell.execute_reply.started": "2025-01-21T02:30:53.841737Z"
    }
   },
   "outputs": [],
   "source": [
    "# 遍历train_ds得到每张图片，计算每个通道的均值和方差\n",
    "def cal_mean_std(ds):\n",
    "    mean = 0.\n",
    "    std = 0.\n",
    "    for img, _ in ds:\n",
    "        mean += img.mean(dim=(1, 2))\n",
    "        std += img.std(dim=(1, 2))\n",
    "    mean /= len(ds)\n",
    "    std /= len(ds)\n",
    "    return mean, std\n",
    "\n",
    "# 经过 normalize 后 均值为0，方差为1\n",
    "# print(cal_mean_std(train_ds))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.781799Z",
     "start_time": "2025-01-20T06:39:38.778866Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:30:53.847720Z",
     "iopub.status.busy": "2025-01-21T02:30:53.847332Z",
     "iopub.status.idle": "2025-01-21T02:30:53.850670Z",
     "shell.execute_reply": "2025-01-21T02:30:53.850094Z",
     "shell.execute_reply.started": "2025-01-21T02:30:53.847700Z"
    }
   },
   "outputs": [],
   "source": [
    "import torch.nn as nn\n",
    "from torch.utils.data.dataloader import DataLoader    \n",
    "\n",
    "batch_size = 16\n",
    "# 从数据集到dataloader\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:19:28.364598500Z",
     "start_time": "2024-07-23T02:19:27.176829700Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:30:53.851702Z",
     "iopub.status.busy": "2025-01-21T02:30:53.851318Z",
     "iopub.status.idle": "2025-01-21T02:30:54.076770Z",
     "shell.execute_reply": "2025-01-21T02:30:54.076154Z",
     "shell.execute_reply.started": "2025-01-21T02:30:53.851673Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([16, 3, 224, 224])\n",
      "torch.Size([16])\n"
     ]
    }
   ],
   "source": [
    "for imgs, labels in train_loader:\n",
    "    print(imgs.shape)\n",
    "    print(labels.shape)\n",
    "    break"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 需要解冻的层可以通过修改代码，解冻某些层的权重，或者使用 `torch.nn.utils.parametrizations.spectral_norm` 进行权重正则化。"
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T07:28:55.526142Z",
     "start_time": "2025-01-20T07:28:55.358597Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:30:54.077515Z",
     "iopub.status.busy": "2025-01-21T02:30:54.077351Z",
     "iopub.status.idle": "2025-01-21T02:30:54.081875Z",
     "shell.execute_reply": "2025-01-21T02:30:54.081382Z",
     "shell.execute_reply.started": "2025-01-21T02:30:54.077500Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "from torchvision.models import resnet50\n",
    "\n",
    "\n",
    "# 加载resnet50模型以及预训练权重，冻结除最后一层外所有层的权重，去除最后一层并添加自定义分类层\n",
    "class ResNet50(nn.Module):\n",
    "    def __init__(self, num_classes=10, frozen=True):\n",
    "        super().__init__()\n",
    "        # 下载预训练权重\n",
    "        self.model = resnet50(weights=\"IMAGENET1K_V2\",) # 这里的weights参数可以选择\"IMAGENET1K_V2\"或None，None表示随机初始化\n",
    "        # 冻结前面的层\n",
    "        if frozen:\n",
    "            for param in self.model.parameters():\n",
    "                param.requires_grad = False # 冻结权重\n",
    "        # for param in self.model.layer4.parameters():\n",
    "        #     param.requires_grad = True  # 解冻 layer4\n",
    "        # for name, param in self.model.named_parameters():\n",
    "        #     if name == \"layer4.2.conv3.weight\":\n",
    "        #         param.requires_grad = True  # 解冻该层\n",
    "        # 添加自定义分类层\n",
    "        # print(self.model)\n",
    "        print(self.model.fc.in_features) # 打印resnet50的最后一层的输入通道数\n",
    "        print(self.model.fc.out_features) # 打印resnet50的最后一层的输出通道数 1000\n",
    "        self.model.fc = nn.Linear(self.model.fc.in_features, num_classes)   # 自定义分类层,把resnet50的最后一层改为num_classes个输出\n",
    "        \n",
    "        \n",
    "    def forward(self, x):\n",
    "        return self.model(x)\n",
    "\n",
    "\n",
    "# for idx, (key, value) in enumerate(ResNet50().named_parameters()):\n",
    "#     print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T07:28:58.912249Z",
     "start_time": "2025-01-20T07:28:58.749737Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T02:30:54.082477Z",
     "iopub.status.busy": "2025-01-21T02:30:54.082333Z",
     "iopub.status.idle": "2025-01-21T02:31:01.147364Z",
     "shell.execute_reply": "2025-01-21T02:31:01.146630Z",
     "shell.execute_reply.started": "2025-01-21T02:30:54.082462Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Downloading: \"https://download.pytorch.org/models/resnet50-11ad3fa6.pth\" to /root/.cache/torch/hub/checkpoints/resnet50-11ad3fa6.pth\n",
      "100%|██████████| 97.8M/97.8M [00:05<00:00, 17.4MB/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "20490"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model = ResNet50(num_classes=10, frozen=True)\n",
    "def count_parameters(model): #计算模型总参数量\n",
    "    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "count_parameters(model)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T07:08:10.086234Z",
     "start_time": "2025-01-20T07:08:10.083273Z"
    }
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:48:29.601561Z",
     "start_time": "2025-01-20T06:48:29.597356Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T02:31:01.148180Z",
     "iopub.status.busy": "2025-01-21T02:31:01.147966Z",
     "iopub.status.idle": "2025-01-21T02:31:01.152771Z",
     "shell.execute_reply": "2025-01-21T02:31:01.152313Z",
     "shell.execute_reply.started": "2025-01-21T02:31:01.148164Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ResNet50(\n",
       "  (model): ResNet(\n",
       "    (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)\n",
       "    (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    (relu): ReLU(inplace=True)\n",
       "    (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n",
       "    (layer1): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "          (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer2): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer3): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (4): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (5): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer4): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))\n",
       "    (fc): Linear(in_features=2048, out_features=10, bias=True)\n",
       "  )\n",
       ")"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:48:57.859769Z",
     "start_time": "2025-01-20T06:48:57.855987Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T02:31:01.153459Z",
     "iopub.status.busy": "2025-01-21T02:31:01.153320Z",
     "iopub.status.idle": "2025-01-21T02:31:01.156737Z",
     "shell.execute_reply": "2025-01-21T02:31:01.156250Z",
     "shell.execute_reply.started": "2025-01-21T02:31:01.153445Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 23528522\n"
     ]
    }
   ],
   "source": [
    "total_params = sum(p.numel() for p in model.parameters() )\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:52:19.752248Z",
     "start_time": "2025-01-20T06:52:19.747379Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T02:31:01.157323Z",
     "iopub.status.busy": "2025-01-21T02:31:01.157172Z",
     "iopub.status.idle": "2025-01-21T02:31:01.162645Z",
     "shell.execute_reply": "2025-01-21T02:31:01.162137Z",
     "shell.execute_reply.started": "2025-01-21T02:31:01.157294Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([1, 2048, 1, 1])"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "m = nn.AdaptiveAvgPool2d(output_size=(1, 1))\n",
    "input = torch.randn(1, 2048, 9, 9)\n",
    "output = m(input)\n",
    "output.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T07:06:55.092794100Z",
     "start_time": "2024-07-23T07:06:55.088043800Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T02:31:01.163555Z",
     "iopub.status.busy": "2025-01-21T02:31:01.163371Z",
     "iopub.status.idle": "2025-01-21T02:31:01.166792Z",
     "shell.execute_reply": "2025-01-21T02:31:01.166321Z",
     "shell.execute_reply.started": "2025-01-21T02:31:01.163537Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2359296"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512*3*3*512"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T03:45:55.270615100Z",
     "start_time": "2024-07-23T03:45:55.261439200Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T02:31:01.167358Z",
     "iopub.status.busy": "2025-01-21T02:31:01.167202Z",
     "iopub.status.idle": "2025-01-21T02:31:01.170533Z",
     "shell.execute_reply": "2025-01-21T02:31:01.170059Z",
     "shell.execute_reply.started": "2025-01-21T02:31:01.167344Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1048576"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512*1*1*2048"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:56:07.349270600Z",
     "start_time": "2024-07-23T02:56:07.337722300Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T02:31:01.172321Z",
     "iopub.status.busy": "2025-01-21T02:31:01.172032Z",
     "iopub.status.idle": "2025-01-21T02:31:01.175170Z",
     "shell.execute_reply": "2025-01-21T02:31:01.174733Z",
     "shell.execute_reply.started": "2025-01-21T02:31:01.172291Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "32.0"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512/16"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练\n",
    "\n",
    "pytorch的训练需要自行实现，包括\n",
    "1. 定义损失函数\n",
    "2. 定义优化器\n",
    "3. 定义训练步\n",
    "4. 训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:59:12.386898800Z",
     "start_time": "2024-07-23T02:59:11.605525600Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:31:01.175759Z",
     "iopub.status.busy": "2025-01-21T02:31:01.175620Z",
     "iopub.status.idle": "2025-01-21T02:31:01.269413Z",
     "shell.execute_reply": "2025-01-21T02:31:01.268920Z",
     "shell.execute_reply.started": "2025-01-21T02:31:01.175745Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "        preds = logits.argmax(axis=-1)    # 验证集预测\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "        \n",
    "    acc = accuracy_score(label_list, pred_list)\n",
    "    return np.mean(loss_list), acc\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### TensorBoard 可视化\n",
    "\n",
    "\n",
    "训练过程中可以使用如下命令启动tensorboard服务。\n",
    "\n",
    "```shell\n",
    "tensorboard \\\n",
    "    --logdir=runs \\     # log 存放路径\n",
    "    --host 0.0.0.0 \\    # ip\n",
    "    --port 8848         # 端口\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-01-21T02:31:01.270281Z",
     "iopub.status.busy": "2025-01-21T02:31:01.270006Z",
     "iopub.status.idle": "2025-01-21T02:31:01.451871Z",
     "shell.execute_reply": "2025-01-21T02:31:01.451411Z",
     "shell.execute_reply.started": "2025-01-21T02:31:01.270264Z"
    }
   },
   "outputs": [],
   "source": [
    "from torch.utils.tensorboard import SummaryWriter\n",
    "\n",
    "\n",
    "class TensorBoardCallback:\n",
    "    def __init__(self, log_dir, flush_secs=10):\n",
    "        \"\"\"\n",
    "        Args:\n",
    "            log_dir (str): dir to write log.\n",
    "            flush_secs (int, optional): write to dsk each flush_secs seconds. Defaults to 10.\n",
    "        \"\"\"\n",
    "        self.writer = SummaryWriter(log_dir=log_dir, flush_secs=flush_secs)\n",
    "\n",
    "    def draw_model(self, model, input_shape):\n",
    "        self.writer.add_graph(model, input_to_model=torch.randn(input_shape))\n",
    "        \n",
    "    def add_loss_scalars(self, step, loss, val_loss):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/loss\", \n",
    "            tag_scalar_dict={\"loss\": loss, \"val_loss\": val_loss},\n",
    "            global_step=step,\n",
    "            )\n",
    "        \n",
    "    def add_acc_scalars(self, step, acc, val_acc):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/accuracy\",\n",
    "            tag_scalar_dict={\"accuracy\": acc, \"val_accuracy\": val_acc},\n",
    "            global_step=step,\n",
    "        )\n",
    "        \n",
    "    def add_lr_scalars(self, step, learning_rate):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/learning_rate\",\n",
    "            tag_scalar_dict={\"learning_rate\": learning_rate},\n",
    "            global_step=step,\n",
    "            \n",
    "        )\n",
    "    \n",
    "    def __call__(self, step, **kwargs):\n",
    "        # add loss\n",
    "        loss = kwargs.pop(\"loss\", None)\n",
    "        val_loss = kwargs.pop(\"val_loss\", None)\n",
    "        if loss is not None and val_loss is not None:\n",
    "            self.add_loss_scalars(step, loss, val_loss)\n",
    "        # add acc\n",
    "        acc = kwargs.pop(\"acc\", None)\n",
    "        val_acc = kwargs.pop(\"val_acc\", None)\n",
    "        if acc is not None and val_acc is not None:\n",
    "            self.add_acc_scalars(step, acc, val_acc)\n",
    "        # add lr\n",
    "        learning_rate = kwargs.pop(\"lr\", None)\n",
    "        if learning_rate is not None:\n",
    "            self.add_lr_scalars(step, learning_rate)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Save Best\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:59:18.296416Z",
     "start_time": "2024-07-23T02:59:18.287854800Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:31:01.452712Z",
     "iopub.status.busy": "2025-01-21T02:31:01.452398Z",
     "iopub.status.idle": "2025-01-21T02:31:01.457396Z",
     "shell.execute_reply": "2025-01-21T02:31:01.456770Z",
     "shell.execute_reply.started": "2025-01-21T02:31:01.452695Z"
    }
   },
   "outputs": [],
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
    "        \"\"\"\n",
    "        Save checkpoints each save_epoch epoch. \n",
    "        We save checkpoint by epoch in this implementation.\n",
    "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
    "\n",
    "        Args:\n",
    "            save_dir (str): dir to save checkpoint\n",
    "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
    "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
    "        \"\"\"\n",
    "        self.save_dir = save_dir\n",
    "        self.save_step = save_step\n",
    "        self.save_best_only = save_best_only\n",
    "        self.best_metrics = -1\n",
    "        \n",
    "        # mkdir\n",
    "        if not os.path.exists(self.save_dir):\n",
    "            os.mkdir(self.save_dir)\n",
    "        \n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return\n",
    "        \n",
    "        if self.save_best_only:\n",
    "            assert metric is not None\n",
    "            if metric >= self.best_metrics:\n",
    "                # save checkpoints\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
    "                # update best metrics\n",
    "                self.best_metrics = metric\n",
    "        else:\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Early Stop"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:59:21.080731100Z",
     "start_time": "2024-07-23T02:59:21.073276Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:31:01.457886Z",
     "iopub.status.busy": "2025-01-21T02:31:01.457747Z",
     "iopub.status.idle": "2025-01-21T02:31:01.461704Z",
     "shell.execute_reply": "2025-01-21T02:31:01.461130Z",
     "shell.execute_reply.started": "2025-01-21T02:31:01.457872Z"
    }
   },
   "outputs": [],
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T03:00:28.829465300Z",
     "start_time": "2024-07-23T02:59:52.601793600Z"
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:31:01.462586Z",
     "iopub.status.busy": "2025-01-21T02:31:01.462270Z",
     "iopub.status.idle": "2025-01-21T02:34:31.183389Z",
     "shell.execute_reply": "2025-01-21T02:34:31.182800Z",
     "shell.execute_reply.started": "2025-01-21T02:31:01.462571Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 45%|████▌     | 621/1380 [03:29<04:15,  2.97it/s, epoch=8]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 9 / global_step 621\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "                preds = logits.argmax(axis=-1)\n",
    "            \n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())    \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"acc\": acc, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss, val_acc = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"acc\": val_acc, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "                    \n",
    "                    # 1. 使用 tensorboard 可视化\n",
    "                    if tensorboard_callback is not None:\n",
    "                        tensorboard_callback(\n",
    "                            global_step, \n",
    "                            loss=loss, val_loss=val_loss,\n",
    "                            acc=acc, val_acc=val_acc,\n",
    "                            lr=optimizer.param_groups[0][\"lr\"],\n",
    "                            )\n",
    "                \n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), metric=val_acc)\n",
    "\n",
    "                    # 3. 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(val_acc)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 20\n",
    "\n",
    "model = ResNet50(num_classes=10)\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "# 2. 定义优化器 采用 sgd\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.0)\n",
    "\n",
    "# 1. tensorboard 可视化\n",
    "if not os.path.exists(\"runs\"):\n",
    "    os.mkdir(\"runs\")\n",
    "# tensorboard_callback = TensorBoardCallback(\"runs/monkeys-resnet50\")\n",
    "# tensorboard_callback.draw_model(model, [1, 3, img_h, img_w])\n",
    "# 2. save best\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(\"checkpoints/monkeys-resnet50\", save_step=len(train_loader), save_best_only=True)\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T03:02:10.300044700Z",
     "start_time": "2024-07-23T03:02:09.443403Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-01-21T02:34:31.184563Z",
     "iopub.status.busy": "2025-01-21T02:34:31.184143Z",
     "iopub.status.idle": "2025-01-21T02:34:31.210853Z",
     "shell.execute_reply": "2025-01-21T02:34:31.210335Z",
     "shell.execute_reply.started": "2025-01-21T02:34:31.184531Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "ename": "ModuleNotFoundError",
     "evalue": "No module named 'torchviz'",
     "output_type": "error",
     "traceback": [
      "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
      "\u001B[0;31mModuleNotFoundError\u001B[0m                       Traceback (most recent call last)",
      "Cell \u001B[0;32mIn[22], line 4\u001B[0m\n\u001B[1;32m      1\u001B[0m \u001B[38;5;66;03m# 画图\u001B[39;00m\n\u001B[1;32m      3\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21;01mtorch\u001B[39;00m\n\u001B[0;32m----> 4\u001B[0m \u001B[38;5;28;01mfrom\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21;01mtorchviz\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;28;01mimport\u001B[39;00m make_dot\n\u001B[1;32m      6\u001B[0m \u001B[38;5;66;03m# Assuming your model is already defined and named 'model'\u001B[39;00m\n\u001B[1;32m      7\u001B[0m \u001B[38;5;66;03m# Construct a dummy input\u001B[39;00m\n\u001B[1;32m      8\u001B[0m dummy_input \u001B[38;5;241m=\u001B[39m torch\u001B[38;5;241m.\u001B[39mrandn(\u001B[38;5;241m1\u001B[39m, \u001B[38;5;241m3\u001B[39m, \u001B[38;5;241m224\u001B[39m, \u001B[38;5;241m224\u001B[39m)  \u001B[38;5;66;03m# Replace with your input shape\u001B[39;00m\n",
      "\u001B[0;31mModuleNotFoundError\u001B[0m: No module named 'torchviz'"
     ]
    }
   ],
   "source": [
    "# 画图\n",
    "\n",
    "import torch\n",
    "from torchviz import make_dot\n",
    "\n",
    "# Assuming your model is already defined and named 'model'\n",
    "# Construct a dummy input\n",
    "dummy_input = torch.randn(1, 3, 224, 224)  # Replace with your input shape\n",
    "\n",
    "# Forward pass to generate the computation graph\n",
    "output = model(dummy_input)\n",
    "\n",
    "# Visualize the model architecture\n",
    "dot = make_dot(output, params=dict(model.named_parameters()))\n",
    "dot.render(\"model_architecture\", format=\"png\")  # Save the visualization as an image\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-01-21T02:35:38.253272Z",
     "iopub.status.busy": "2025-01-21T02:35:38.252957Z",
     "iopub.status.idle": "2025-01-21T02:35:38.438724Z",
     "shell.execute_reply": "2025-01-21T02:35:38.438249Z",
     "shell.execute_reply.started": "2025-01-21T02:35:38.253251Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzoAAAHACAYAAABqJx3iAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAu11JREFUeJzs3Xd4m+XV+PHvo2HJe8R7JM7ee5GEESADAmG3FCirjBZIXyD0B83bstvSwWoLhbdQCm3ZUEZJGmICATIge5E9POLteFu29u+PR5KXZMu2bNny+VyXr9jS80j3LTv2c3Tu+xzF6XQ6EUIIIYQQQogQogn2AIQQQgghhBAi0CTQEUIIIYQQQoQcCXSEEEIIIYQQIUcCHSGEEEIIIUTIkUBHCCGEEEIIEXIk0BFCCCGEEEKEHAl0hBBCCCGEECFHAh0hhBBCCCFEyNEFewD+cDgcFBUVER0djaIowR6OEEIMGk6nk7q6OtLT09Fo5L0xN/m7JIQQwePv36YBEegUFRWRlZUV7GEIIcSgVVBQQGZmZrCH0W/I3yUhhAi+zv42DYhAJzo6GlAnExMT0+XzrVYr69atY8mSJej1+kAPr0+F0lwgtOYTSnMBmU9/1pdzqa2tJSsry/N7WKjk71JrMp/+K5TmAqE1n1CaC/TPv00DItBxLwuIiYnp9h+UiIgIYmJiBvwPUijNBUJrPqE0F5D59GfBmIssz2pN/i61JvPpv0JpLhBa8wmluUD//NskC66FEEIIIYQQIUcCHSGEEEIIIUTIkUBHCCGEEEIIEXIGxB4dIUT/5HQ6sdls2O32YA+lU1arFZ1OR1NT04AYb0cCORetVotOp5M9OEIIIUKOBDpCiG6xWCwUFxdjMpmCPRS/OJ1OUlNTKSgoGPAX9YGeS0REBGlpaYSFhQVgdEIIIUT/IIGOEKLLHA4HJ0+eRKvVkp6eTlhYWL8PHhwOB/X19URFRQ34xpeBmovT6cRisVBeXs7JkycZPXr0gH9thBBCCDcJdIQQXWaxWHA4HGRlZRERERHs4fjF4XBgsVgwGo0D/mI+kHMJDw9Hr9eTl5fneUwhhBAiFAzsv/ZCiKAa6AGDUMn3UQghRCiSv25CCCGEEEKIkCOBjhBCCCGEECLkSKAjhBDdlJ2dzbPPPhuQx9qwYQOKolBdXR2QxxtMvvrqK5YvX056ejqKovDhhx92es6GDRuYMWMGBoOBUaNG8eqrr/b6OIUQQvQtCXSEEIPKeeedxz333BOQx9q2bRu33357QB5LdF9DQwNTp07l+eef9+v4kydPctFFF3Huueeye/du7rnnHm699VY+/fTTXh6pEEKIviRV14QQogWn04ndbken6/zXY1JSUh+MSHTmwgsv5MILL/T7+BdffJHhw4fz1FNPATB+/Hg2btzIM888w9KlS3trmEIIIfpYyAc6Gw6X8eSnhwm3algW7MEIEaKcTieNVntQnjtcr/W7h8+dd97Jl19+yZdffskf//hHAP7+979z8803s2bNGn75y1+yb98+1q1bR1ZWFitXruSbb76hoaGB8ePH88QTT7Bo0SLP42VnZ3PPPfd4MkSKovDSSy+xevVqPv30UzIyMnjqqae45JJLujW3999/n4ceeohjx46RlpbGT3/6U+677z7P/S+88ALPPvssBQUFxMbGctZZZ/Hee+8B8N577/Hoo49y7NgxIiIimD59Oh999BGRkZHdGkso2bJlS6vvI8DSpUs7zPSZzWbMZrPn69raWgCsVitWq7XLY3Cf051ze0N+pYnHVx/ip+eOZEpmbJfP/91/D7F6j5bnj2/q9P9jXISeP1w5mbTYnpcydzqdPPyfg+zMr+7xY7V93Lr61vNRFIUbzhjK92ZmdHjuXzacoLzezIPLxqHRdK2/2NHSen7938Pcfd5Ipg+N83mc3eHkwY8PMD41muvPGNrhY3r7Wfv0u1L+8uUJ7A5nl8bXm6IMOn516QRGJUd1eFzb+TgcTh74934OltS1O3ZaViyPXzKhy33ejpXV85v/Hua+xaOZmB7j8zizzcHKd/eSd7p94+w5wxN4cNnYDp+7J78HGsw27nlnL8U1TV0+t7d4+38DMGNoHI8uHx/Qfnv+vmYhH+goisL+olpSwvt3M0MhBrJGq50JDwVn2c+Bx5YSEebfr7InnniC3NxcJk2axGOPPQbAd999B8DPf/5znnzySUaMGEF8fDwFBQUsW7aMX//61xgMBv7xj3+wfPlyDh8+zNChvi8sHn30UX7/+9/zhz/8gT//+c9cd9115OXlkZCQ0KV57dixg+9///s88sgjXH311WzevJk777yTIUOGcMMNN7Br1y7uvvtu/vnPfzJ//nwqKyv5+uuvASguLuaaa67h97//PZdffjl1dXV8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",
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    fig_num = len(train_df.columns)\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):    \n",
    "        axs[idx].plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        axs[idx].grid()\n",
    "        axs[idx].legend()\n",
    "        # axs[idx].set_xticks(range(0, train_df.index[-1], 5000))\n",
    "        # axs[idx].set_xticklabels(map(lambda x: f\"{int(x/1000)}k\", range(0, train_df.index[-1], 5000)))\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "    \n",
    "    plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=10)  #横坐标是 steps"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 评估"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-01-21T02:35:59.399251Z",
     "iopub.status.busy": "2025-01-21T02:35:59.398763Z",
     "iopub.status.idle": "2025-01-21T02:36:04.302282Z",
     "shell.execute_reply": "2025-01-21T02:36:04.301732Z",
     "shell.execute_reply.started": "2025-01-21T02:35:59.399229Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.3100\n",
      "accuracy: 0.9890\n"
     ]
    }
   ],
   "source": [
    "# dataload for evaluating\n",
    "\n",
    "# load checkpoints\n",
    "model.load_state_dict(torch.load(\"checkpoints/monkeys-resnet50/best.ckpt\",weights_only=True, map_location=\"cpu\"))\n",
    "\n",
    "model.eval()\n",
    "loss, acc = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\\naccuracy: {acc:.4f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
